#define __STDC_LIMIT_MACROS
#include <errno.h>
#include <limits.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>

#include "./hash_map/hash_map.h"
#include "./slab_allocator/slab_allocator.h"
#include "./swapping/swapping.h"
#include "../../logger/logger.h"

#define MAX_BLOCK_NUM 1000

#define MIN_BLOCK_SZ 64
#define MAX_BLOCK_SZ 65536

typedef struct mem_block {
	char *ptr;
	size_t id;
	size_t size;
	short int ref_cnt;
	short int released;
	off_t offset;
	struct mem_block *next;
} mem_block_t;

static struct {
	off_t swap_size;
	uint64_t mem_size;
	uint64_t used_mem;
	pthread_mutex_t mutex;
} memalloc;

mem_block_t *block_list = NULL;
static mem_block_t blocks[MAX_BLOCK_NUM];

static int ma_inited = 0;
static size_t cur_id = 1;

static hash_map_t hm_memalloc;

int ma_init(size_t mem, off_t swap, const char *swap_path) {
	int i;
	if (ma_inited) {
		errno = EALREADY;
		return 0;
	}
	if (!sa_init(mem)) {
		errno = ENOMEM;
		return 0;
	}
	if ((off_t)mem > swap) {
		errno = EBADR;
		return 0;
	}
	perror("");
	if (!swap_init(swap, swap_path)) {
		perror("");
		return 0;
	}
	if (0 != pthread_mutex_init(&memalloc.mutex, NULL)) {
		return 0;
	}
	map_init(&hm_memalloc);

	for (i = 0; i < MAX_BLOCK_NUM; ++i) {
		blocks[i].next = block_list;
		block_list = &blocks[i];
	}
	memalloc.mem_size = mem;
	memalloc.swap_size = swap;
	memalloc.used_mem = 0;
	ma_inited = 1;
	LOG_DEBUG("MEMALLOC: Memory allocator was initialized");
	return 1;
}

void ma_deinit() {
	if (!ma_inited)
		return;
	sa_deinit();
	swap_deinit();
	block_list = NULL;
	cur_id = 1;
	ma_inited = 0;
	pthread_mutex_destroy(&memalloc.mutex);
	LOG_DEBUG("MEMALLOC: Memory allocator was deinitialized");
}

size_t ma_alloc(size_t sz) {
	if (!ma_inited) {
		errno = ECANCELED;
		return 0;
	}
	if (!sz) {
		errno = EFAULT;
		return 0;
	}
	if (sz > memalloc.mem_size) {
		errno = E2BIG;
		return 0;
	}
	if (!cur_id) {
		errno = EMLINK;
		return 0;
	}

	pthread_mutex_lock(&memalloc.mutex);

	if (memalloc.mem_size + memalloc.swap_size - memalloc.used_mem < sz) {
		errno = ENOMEM;
		return 0;
	}

	if ((sz < MIN_BLOCK_SZ) || (sz > MAX_BLOCK_SZ)) {
		errno = EINVAL;
		return 0;
	}
	void *ptr;
	if ((!(ptr = sa_alloc(sz))) || (!block_list)) {
		errno = ENOMEM;
		pthread_mutex_unlock(&memalloc.mutex);
		return 0;
	}
	mem_block_t *mem = block_list;
	block_list = block_list->next;
	mem->ptr = (char *)ptr;
	mem->id = cur_id++;
	mem->released = 0;
	mem->next = NULL;
	mem->size = sz;
	mem->ref_cnt = 0;
	memalloc.used_mem += sz;
	if (!map_insert(&hm_memalloc, mem->id, mem)) {
		pthread_mutex_unlock(&memalloc.mutex);
		return 0;
	}
	pthread_mutex_unlock(&memalloc.mutex);
	LOG_DEBUG("MEMALLOC: Memory was allocated with id %d", cur_id - 1);
	return cur_id - 1;
}

int ma_free(size_t id) {
	if (!ma_inited) {
		errno = ECANCELED;
		return 0;
	}
	pthread_mutex_lock(&memalloc.mutex);
	mem_block_t *mem = (mem_block_t *)map_delete(&hm_memalloc, id);
	if (!mem) {
		errno = EFAULT;
		pthread_mutex_unlock(&memalloc.mutex);
		return 0;
	}
	if (mem->released) {
		errno = EBUSY;
		pthread_mutex_unlock(&memalloc.mutex);
		return 0;
	}
	if ((!id) || (id >= cur_id)) {
		errno = EFAULT;
		pthread_mutex_unlock(&memalloc.mutex);
		return 0;
	}
	sa_free(mem->ptr);
	mem->next = block_list;
	block_list = mem;
	memalloc.used_mem -= mem->size;
	pthread_mutex_unlock(&memalloc.mutex);
	LOG_DEBUG("MEMALLOC: Memory with id %d was freed", id);
	return 1;
}

void *ma_get(size_t id) {
	if (!ma_inited) {
		errno = ECANCELED;
		return NULL;
	}

	pthread_mutex_lock(&memalloc.mutex);
	mem_block_t *mem = (mem_block_t *)map_get(&hm_memalloc, id);
	if (!mem) {
		errno = EINVAL;
		pthread_mutex_unlock(&memalloc.mutex);
		return NULL;
	}
	++mem->ref_cnt;
	if (!mem->released)
		return mem->ptr;
	if (!(mem->ptr = sa_alloc(mem->size))) {
		errno = ENOMEM;
		pthread_mutex_unlock(&memalloc.mutex);
		return NULL;
	}
	if (!swap_out(mem->ptr, mem->offset, mem->size)) {
		pthread_mutex_unlock(&memalloc.mutex);
		return NULL;
	}
	mem->released = 0;
	pthread_mutex_unlock(&memalloc.mutex);
	LOG_DEBUG("MEMALLOC: Attaching memory with id = %d to address = %p", id, mem->ptr);
	return mem->ptr;
}

int ma_release(size_t id) {
	if (!ma_inited) {
		errno = ECANCELED;
		return 0;
	}
	pthread_mutex_lock(&memalloc.mutex);
	mem_block_t *mem = (mem_block_t *)map_get(&hm_memalloc, id);

	if (!mem) {
		errno = EINVAL;
		pthread_mutex_unlock(&memalloc.mutex);
		return 0;
	}
	if (mem->released) {
		errno = EFAULT;
		pthread_mutex_unlock(&memalloc.mutex);
		return 0;
	}
	if ((mem->offset = swap_in(mem->ptr, mem->size)) < 0) {
		pthread_mutex_unlock(&memalloc.mutex);
		return 0;
	}
	--mem->ref_cnt;
	if (!mem->ref_cnt) {
		sa_free(mem->ptr);
		mem->released = 1;
		LOG_DEBUG("MEMALLOC: Releasing memory with id = %d from address = %p", id, mem->ptr);
		mem->ptr = NULL;
	}
	pthread_mutex_unlock(&memalloc.mutex);
	return 1;
}
